package fpga

import (
	"fmt"
	"fpga-vboard/config"
	"fpga-vboard/logger"
	"fpga-vboard/pins"
	"fpga-vboard/utils"
	"os"
	"path"
	"path/filepath"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"text/template"
)

var tempPath string
var dstPath string

func init() {
	tempPath = path.Join(config.AppCfg.AppPath, "template", "verilog")
	dstPath = path.Join(config.AppCfg.AppPath, "runtemp", "verilog")
	os.MkdirAll(dstPath, os.ModePerm)
}

type Signal struct {
	Direction string // IN/OUT
	Width     string
	VarName   string
	BitSize   uint
	Type      string
	IsArray   bool
}

func parseSignal(line string) *Signal {
	re := regexp.MustCompile(`(VL_(IN|OUT)(\d*|W)?)\s*\(&(\w+),\s*(\d+),\s*(\d+)(?:,\s*(\d+))?\)`)
	matches := re.FindStringSubmatch(line)
	if matches == nil {
		return nil
	}

	msb, _ := strconv.Atoi(matches[5])
	lsb, _ := strconv.Atoi(matches[6])

	sig := &Signal{
		Direction: matches[2],
		Width:     matches[3],
		VarName:   matches[4],
		BitSize:   uint(msb - lsb + 1),
		IsArray:   false,
	}
	switch sig.Width {
	case "8":
		sig.Type = "CData"
	case "16":
		sig.Type = "SData"
	case "64":
		sig.Type = "QData"
	case "W":
		sig.Type = "EData"
		sig.IsArray = true
	default:
		sig.Type = "IData"
	}
	return sig
}

type VerilatorConfig struct {
	VerilatorModuleFilePath string `label:"verilator module file path" required:"true" type:"fileInput" filters:"*.v;*.sv"`
	VerilatorTopModule      string
	VerilogModulePaths      []string `label:"depends modules path" required:"false" type:"dirArray"`
	VerilatorPath           string   `label:"verilator home path" required:"true" type:"dirInput"`
	VerilatorIncludePath    string
	VerilatorCommand        string
	VerilatorTrace          bool   `label:"trace enable" required:"true" type:"onoff"`
	VerilatorTraceFile      string `label:"trace file dest path" required:"false" type:"fileInput" filters:"*.vcd"`
	Inputs                  []*Signal
	Outputs                 []*Signal
}

type VerilatorSynth struct {
	config *VerilatorConfig
}

func (v *VerilatorSynth) Synthesize(cfg *SynthesisConfig) (*SynthesisResult, error) {
	v.config = cfg.ExtraArgs.(*VerilatorConfig)
	utils.AddPath(path.Join(v.config.VerilatorPath, "bin"))
	v.config.VerilatorIncludePath = path.Join(v.config.VerilatorPath, "include")
	v.config.VerilatorCommand = "verilator_bin"
	v.config.VerilatorModuleFilePath = strings.ReplaceAll(v.config.VerilatorModuleFilePath, "\\", "/")
	for idx, modulePath := range v.config.VerilogModulePaths {
		v.config.VerilogModulePaths[idx] = strings.ReplaceAll(modulePath, "\\", "/")
	}
	v.config.VerilatorTraceFile = strings.ReplaceAll(v.config.VerilatorTraceFile, "\\", "/")
	v.config.VerilatorTopModule = filepath.Base(v.config.VerilatorModuleFilePath)
	v.config.VerilatorTopModule, _, _ = strings.Cut(v.config.VerilatorTopModule, ".")
	err := v.Generate()
	if err != nil {
		return nil, err
	}
	res := &SynthesisResult{
		LibPath:    path.Join(dstPath, fmt.Sprintf("lib%s.so", v.config.VerilatorTopModule)),
		InputPins:  make([]*pins.PinDef, len(v.config.Inputs)),
		OutputPins: make([]*pins.PinDef, len(v.config.Outputs)),
	}
	if runtime.GOOS == "windows" {
		res.LibPath = filepath.Clean(path.Join(dstPath, fmt.Sprintf("lib%s.dll", v.config.VerilatorTopModule)))
	}
	for idx, input := range v.config.Inputs {
		res.InputPins[idx] = pins.NewPinDef(input.VarName, input.BitSize, pins.PinDirectionInput)
	}
	for idx, output := range v.config.Outputs {
		res.OutputPins[idx] = pins.NewPinDef(output.VarName, output.BitSize, pins.PinDirectionOutput)
	}
	return res, nil
}

func (v *VerilatorSynth) ReadPins() error {
	headerFile := path.Join(dstPath, "varlator_obj", v.config.VerilatorTopModule+".h")
	hInfo, err := os.ReadFile(headerFile)
	if err != nil {
		logger.FatalEvent("Failed to read header file: %v, error: %v", headerFile, err)
		return err
	}
	for _, line := range strings.Split(string(hInfo), "\n") {
		line = strings.TrimSpace(line)
		signal := parseSignal(line)
		if signal == nil {
			continue
		}
		if signal.Direction == "IN" {
			v.config.Inputs = append(v.config.Inputs, signal)
		} else if signal.Direction == "OUT" {
			v.config.Outputs = append(v.config.Outputs, signal)
		}
	}
	return nil
}

func (v *VerilatorSynth) Generate() error {
	err := v.GenerateFile("module.f")
	if err != nil {
		return err
	}
	err = v.GenerateFile("Makefile")
	if err != nil {
		return err
	}
	msg, err := utils.RunCmdWithParams(dstPath, "make verilated")
	if err != nil {
		logger.FatalEvent("Failed to generate verilated: %v, error: %v", msg, err)
		return err
	}
	err = v.ReadPins()
	if err != nil {
		return err
	}
	err = v.GenerateFile("plugin.cpp")
	if err != nil {
		return err
	}
	msg, err = utils.RunCmdWithParams(dstPath, "make plugin")
	if err != nil {
		logger.FatalEvent("Failed to generate plugin: %v, error: %v", msg, err)
		return err
	}
	return nil
}

func (v *VerilatorSynth) GenerateFile(file string) error {
	// top config file
	cFileTemplate := path.Join(tempPath, file+".tmpl")
	cFile := path.Join(dstPath, file)
	templateInfo, err := os.ReadFile(cFileTemplate)
	if err != nil {
		logger.FatalEvent("Failed to read template file: %v, error: %v", cFileTemplate, err)
		return err
	}
	templateInst, err := template.New("verilator").Parse(string(templateInfo))
	if err != nil {
		logger.FatalEvent("Failed to parse template: %v, error: %v", cFileTemplate, err)
		return err
	}
	os.Remove(cFile)
	cFileInst, err := os.OpenFile(cFile, os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		logger.FatalEvent("Failed to open file: %v, error: %v", file, err)
		return err
	}
	err = templateInst.Execute(cFileInst, v.config)
	if err != nil {
		logger.FatalEvent("Failed to generate file: %v, error: %v", file, err)
		return err
	}
	return nil
}
